package org.lex.game.sudoku.model;

import java.util.Arrays;

import javax.swing.event.EventListenerList;

import ms.sudoku.util.IntSet;

import org.lex.game.sudoku.model.SudokuModelListener.Cause;
import org.lex.game.sudoku.model.builder.SudokuBuilder;

import common.Sudokus;

public class DefaultSudokuModel implements SudokuModel {

	private EventListenerList listenerList = null;

	// FIXME boxSize is NOT FIXED!!!
	private int boxSize = 3;

	private int[][] originalBoard = null;
	private int[][] workingBoard = null;

	private final SudokuModelSupport support;
	private final HintCache hintCache;

	public DefaultSudokuModel() {
		this.listenerList = new EventListenerList();

		this.originalBoard = new int[Sudokus.DEFAULT_GRID_SIZE][Sudokus.DEFAULT_GRID_SIZE];
		this.workingBoard = new int[Sudokus.DEFAULT_GRID_SIZE][Sudokus.DEFAULT_GRID_SIZE];
		for (int row = 0; row < Sudokus.DEFAULT_GRID_SIZE; row++) {
			Arrays.fill(this.originalBoard[row], Sudokus.EMPTY_NUMBER);
			Arrays.fill(this.workingBoard[row], Sudokus.EMPTY_NUMBER);
		}

		this.support = new SudokuModelSupport(this);
		this.hintCache = new HintCache(this);
		// this.newGame();
	}

	// ///////////////////////
	// change model properties
	// ///////////////////////
	@Override
	public void newGame(SudokuBuilder builder) {
		int gridSize = this.getGridSize();
		// clear
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				this.originalBoard[row][col] = Sudokus.EMPTY_NUMBER;
				this.workingBoard[row][col] = Sudokus.EMPTY_NUMBER;
			}
		}
		// build
		builder.build(this);

		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				this.originalBoard[row][col] = this.workingBoard[row][col];
			}
		}
		// fire event
		this.fireModelChanged();
		System.out.println(this);
	}

	@Override
	public void reset() {
		int gridSize = this.getGridSize();
		// clear
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				this.workingBoard[row][col] = this.originalBoard[row][col];
			}
		}
		// fire event
		this.fireModelChanged();
	}

	@Override
	public void set(Cause cause, int row, int column, int newValue) {
		int oldValue = this.get(row, column);
		this.workingBoard[row][column] = newValue;
		this.fireCellChanged(cause, row, column, oldValue, newValue);
	}

	// ////////////////////
	// get model properties
	// ////////////////////

	@Override
	public int getBoxSize() {
		return this.boxSize;
	}

	@Override
	public int getGridSize() {
		return boxSize * boxSize;
	}

	@Override
	public int get(int row, int column) {
		return this.workingBoard[row][column];
	}

	@Override
	public int getOriginalEmptyCellsCount() {
		return this.getNumberCount(this.originalBoard, Sudokus.EMPTY_NUMBER);
	}

	@Override
	public int getCurrentEmptyCellsCount() {
		return this.getNumberCount(this.workingBoard, Sudokus.EMPTY_NUMBER);
	}

	private int getNumberCount(int[][] array, final int number) {
		int count = 0;
		int gridSize = this.getGridSize();
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				if (number == array[row][col]) {
					count++;
				}
			}
		}
		return count;
	}

	@Override
	public IntSet getHints(int row, int column) {
		// return this.hintCache.getHints(row, column);
		return this.support.getHints(row, column);
	}

	@Override
	public int[][] toArray() {
		return this.workingBoard.clone();
	}

	// //////////////////////
	// check model properties
	// //////////////////////
	@Override
	public boolean isCompleted() {
		return 0 == this.getCurrentEmptyCellsCount();
	}

	@Override
	public boolean isEditable(int row, int column) {
		return Sudokus.isEmpty(this.originalBoard[row][column]);
	}

	/**
	 * 检查一个单元(row, column)所在的行、列、块中是否有重复数字
	 * 
	 * @param row
	 * @param column
	 * @param findAllInvalid
	 * @return
	 */
	@Override
	public CheckResult checkCell(int row, int column, boolean findAllInvalid) {
		return this.support.checkCell(row, column, findAllInvalid);
	}

	@Override
	public CheckResult checkAll(boolean findAllInvalid) {
		return this.support.checkAll(findAllInvalid);
	}

	@Override
	public boolean checkCanFillWith(int row, int col, int num) {
		return this.support.checkCanFillWith(row, col, num);
	}

	// //////////////////////
	// event support
	// //////////////////////
	@Override
	public void addListener(SudokuModelListener listener) {
		this.listenerList.add(SudokuModelListener.class, listener);
	}

	@Override
	public void removeListener(SudokuModelListener listener) {
		this.listenerList.remove(SudokuModelListener.class, listener);
	}

	protected void fireModelChanged() {
		// Guaranteed to return a non-null array
		Object[] listeners = this.listenerList.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == SudokuModelListener.class) {
				((SudokuModelListener) listeners[i + 1]).modelChanged(this);
			}
		}
	}

	protected void fireCellChanged(Cause cause, int row, int col, int oldValue, int newValue) {
		// Guaranteed to return a non-null array
		Object[] listeners = this.listenerList.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == SudokuModelListener.class) {
				((SudokuModelListener) listeners[i + 1]).cellChanged(this, cause, row, col, oldValue, newValue);
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		String nl = System.getProperty("line.separator", "\n");
		int gridSize = this.getGridSize();
		buf.append("Sudoku model: ").append(gridSize).append(" x ").append(gridSize).append(nl);
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				int original = this.originalBoard[row][col];
				int working = this.workingBoard[row][col];
				if (Sudokus.EMPTY_NUMBER != original) {
					buf.append(' ').append(original).append(' ');
				} else if (Sudokus.EMPTY_NUMBER != working) {
					buf.append('[').append(working).append(']');
				} else {
					buf.append("[_]");
				}
				buf.append(' ');
			}
			buf.append(nl);
		}
		return buf.toString();
	}

	@Override
	public String dump() {
		StringBuilder buf = new StringBuilder();
		int gridSize = this.getGridSize();
		String nl = System.getProperty("line.separator", "\n");
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				buf.append(this.workingBoard[row][col]);
				buf.append(", ");
			}
			buf.append(nl);
		}
		return buf.toString();
	}
}
